<?php
/**
 * Piwik - Open source web analytics
 *
 * @link http://piwik.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 *
 * @category Piwik_Plugins
 * @package Piwik_Annotations
 */

/**
 * @see plugins/Annotations/AnnotationList.php
 */
require_once PIWIK_INCLUDE_PATH.'/plugins/Annotations/AnnotationList.php';

/**
 * API for annotations plugin. Provides methods to create, modify, delete & query
 * annotations.
 * 
 * @package Piwik_Annotations
 */
class Piwik_Annotations_API
{
	static private $instance = null;
	
	/**
	 * Returns this API's singleton instance.
	 * 
	 * @return Piwik_Annotations_API
	 */
	static public function getInstance()
	{
		if (self::$instance == null)
		{
			self::$instance = new self;
		}
		return self::$instance;
	}
	
	/**
	 * Create a new annotation for a site.
	 * 
	 * @param string $idSite The site ID to add the annotation to.
	 * @param string $date The date the annotation is attached to.
	 * @param string $note The text of the annotation.
	 * @param string $starred Either 0 or 1. Whether the annotation should be starred.
	 * @return array Returns an array of two elements. The first element (indexed by
	 *               'annotation') is the new annotation. The second element (indexed
	 *               by 'idNote' is the new note's ID).
	 */
	public function add( $idSite, $date, $note, $starred = 0 )
	{
		$this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot add one note to multiple sites.");
		$this->checkDateIsValid($date);
		$this->checkUserCanAddNotesFor($idSite);
		
		// add, save & return a new annotation
		$annotations = new Piwik_Annotations_AnnotationList($idSite);
		
		$newAnnotation = $annotations->add($idSite, $date, $note, $starred);
		$annotations->save($idSite);
		
		return $newAnnotation;
	}
	
	/**
	 * Modifies an annotation for a site and returns the modified annotation
	 * and its ID.
	 * 
	 * If the current user is not allowed to modify an annotation, an exception
	 * will be thrown. A user can modify a note if:
	 *  - the user has admin access for the site, OR
	 *  - the user has view access, is not the anonymous user and is the user that
	 *    created the note
	 * 
	 * @param string $idSite The site ID to add the annotation to.
	 * @param string $idNote The ID of the note.
	 * @param string|null $date The date the annotation is attached to. If null, the annotation's
	 *                          date is not modified.
	 * @param string|null $note The text of the annotation. If null, the annotation's text
	 *                          is not modified.
	 * @param string|null $starred Either 0 or 1. Whether the annotation should be starred.
	 *                             If null, the annotation is not starred/un-starred.
	 * @return array Returns an array of two elements. The first element (indexed by
	 *               'annotation') is the new annotation. The second element (indexed
	 *               by 'idNote' is the new note's ID).
	 */
	public function save( $idSite, $idNote, $date = null, $note = null, $starred = null )
	{
		$this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot modify more than one note at a time.");
		$this->checkDateIsValid($date, $canBeNull = true);
		
		// get the annotations for the site
		$annotations = new Piwik_Annotations_AnnotationList($idSite);
		
		// check permissions
		$this->checkUserCanModifyOrDelete($idSite, $annotations->get($idSite, $idNote));
		
		// modify the annotation, and save the whole list
		$annotations->update($idSite, $idNote, $date, $note, $starred);
		$annotations->save($idSite);
		
		return $annotations->get($idSite, $idNote);
	}
	
	/**
	 * Removes an annotation from a site's list of annotations.
	 * 
	 * If the current user is not allowed to delete the annotation, an exception
	 * will be thrown. A user can delete a note if:
	 *  - the user has admin access for the site, OR
	 *  - the user has view access, is not the anonymous user and is the user that
	 *    created the note
	 * 
	 * @param string $idSite The site ID to add the annotation to.
	 * @param string $idNote The ID of the note to delete.
	 */
	public function delete( $idSite, $idNote )
	{
		$this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot delete multiple notes.");
		
		$annotations = new Piwik_Annotations_AnnotationList($idSite);
		
		// check permissions
		$this->checkUserCanModifyOrDelete($idSite, $annotations->get($idSite, $idNote));
		
		// remove the note & save the list
		$annotations->remove($idSite, $idNote);
		$annotations->save($idSite);
	}
	
	/**
	 * Returns a single note for one site.
	 * 
	 * @param string $idSite The site ID to add the annotation to.
	 * @param string $idNote The ID of the note to get.
	 * @return array The annotation. It will contain the following properties:
	 *               - date: The date the annotation was recorded for.
	 *               - note: The note text.
	 *               - starred: Whether the note is starred or not.
	 *               - user: The user that created the note.
	 *               - canEditOrDelete: Whether the user that called this method can edit or
	 *                                  delete the annotation returned.
	 */
	public function get( $idSite, $idNote )
	{
		$this->checkSingleIdSite($idSite, $extraMessage = "Note: Specify only one site ID when getting ONE note.");
		Piwik::checkUserHasViewAccess($idSite);
		
		// get single annotation
		$annotations = new Piwik_Annotations_AnnotationList($idSite);
		return $annotations->get($idSite, $idNote);
	}
	
	/**
	 * Returns every annotation for a specific site within a specific date range.
	 * The date range is specified by a date, the period type (day/week/month/year)
	 * and an optional number of N periods in the past to include.
	 * 
	 * @param string $idSite The site ID to add the annotation to. Can be one ID or
	 *                       a list of site IDs.
	 * @param string|false $date The date of the period.
	 * @param string $period The period type.
	 * @param int|false $lastN Whether to include the last N number of periods in the
	 *                         date range or not.
	 * @return array An array that indexes arrays of annotations by site ID. ie,
	 *               array(
	 *                 5 => array(
	 *                   array(...), // annotation #1
	 *                   array(...), // annotation #2
	 *                 ),
	 *                 8 => array(...)
	 *               )
	 */
	public function getAll( $idSite, $date = false, $period = 'day', $lastN = false )
	{
		Piwik::checkUserHasViewAccess($idSite);
		
		$annotations = new Piwik_Annotations_AnnotationList($idSite);
		
		// if date/period are supplied, determine start/end date for search
		list($startDate, $endDate) = self::getDateRangeForPeriod($date, $period, $lastN);
		
		return $annotations->search($startDate, $endDate);
	}
	
	/**
	 * Returns the count of annotations for a list of periods, including the count of
	 * starred annotations.
	 * 
	 * @param string $idSite The site ID to add the annotation to.
	 * @param string|false $date The date of the period.
	 * @param string $period The period type.
	 * @param int|false $lastN Whether to get counts for the last N number of periods or not.
	 * @return array An array mapping site IDs to arrays holding dates & the count of
	 *               annotations made for those dates. eg,
	 *               array(
	 *                 5 => array(
	 *                   array('2012-01-02', array('count' => 4, 'starred' => 2)),
	 *                   array('2012-01-03', array('count' => 0, 'starred' => 0)),
	 *                   array('2012-01-04', array('count' => 2, 'starred' => 0)),
	 *                 ),
	 *                 6 => array(
	 *                   array('2012-01-02', array('count' => 1, 'starred' => 0)),
	 *                   array('2012-01-03', array('count' => 4, 'starred' => 3)),
	 *                   array('2012-01-04', array('count' => 2, 'starred' => 0)),
	 *                 ),
	 *                 ...
	 *               )
	 */
	public function getAnnotationCountForDates( $idSite, $date, $period, $lastN = false, $getAnnotationText = false )
	{
		Piwik::checkUserHasViewAccess($idSite);
		
		// get start & end date for request. lastN is ignored if $period == 'range'
		list($startDate, $endDate) = self::getDateRangeForPeriod($date, $period, $lastN);
		if ($period == 'range')
		{
			$period = 'day';
		}
		
		// create list of dates
		$dates = array();
		for (; $startDate->getTimestamp() <= $endDate->getTimestamp(); $startDate = $startDate->addPeriod(1, $period))
		{
			$dates[] = $startDate;
		}
		// we add one for the end of the last period (used in for loop below to bound annotation dates)
		$dates[] = $startDate;
		
		// get annotations for the site
		$annotations = new Piwik_Annotations_AnnotationList($idSite);
		
		// create result w/ 0-counts
		$result = array();
		for ($i = 0; $i != count($dates) - 1; ++$i)
		{
			$date = $dates[$i];
			$nextDate = $dates[$i + 1];
			$strDate = $date->toString();
			
			foreach ($annotations->getIdSites() as $idSite)
			{
				$result[$idSite][$strDate] = $annotations->count($idSite, $date, $nextDate);
				
				// if only one annotation, return the one annotation's text w/ the counts
				if ($getAnnotationText
					&& $result[$idSite][$strDate]['count'] == 1)
				{
					$annotationsForSite = $annotations->search(
						$date, Piwik_Date::factory($nextDate->getTimestamp() - 1), $idSite);
					$annotation = reset($annotationsForSite[$idSite]);
					
					$result[$idSite][$strDate]['note'] = $annotation['note'];
				}
			}
		}
		
		// convert associative array into array of pairs (so it can be traversed by index)
		$pairResult = array();
		foreach ($result as $idSite => $counts)
		{
			foreach ($counts as $date => $count)
			{
				$pairResult[$idSite][] = array($date, $count);
			}
		}
		return $pairResult;
	}
	
	/**
	 * Throws if the current user is not allowed to modify or delete an annotation.
	 * 
	 * @param int $idSite The site ID the annotation belongs to.
	 * @param array $annotation The annotation.
	 * @throws Exception if the current user is not allowed to modify/delete $annotation.
	 */
	private function checkUserCanModifyOrDelete( $idSite, $annotation )
	{
		if (!$annotation['canEditOrDelete'])
		{
			throw new Exception(Piwik_Translate('Annotations_YouCannotModifyThisNote'));
		}
	}
	
	/**
	 * Throws if the current user is not allowed to create annotations for a site.
	 * 
	 * @param int $idSite The site ID.
	 * @throws Exception if the current user is anonymous or does not have view access
	 *                   for site w/ id=$idSite.
	 */
	private static function checkUserCanAddNotesFor( $idSite )
	{
		if (!Piwik_Annotations_AnnotationList::canUserAddNotesFor($idSite))
		{
			throw new Exception("The current user is not allowed to add notes for site #$idSite.");
		}
	}
	
	/**
	 * Returns start & end dates for the range described by a period and optional lastN
	 * argument.
	 * 
	 * @param string $date|false The start date of the period (or the date range of a range
	 *                           period).
	 * @param string $period The period type ('day', 'week', 'month', 'year' or 'range').
	 * @param int|false $lastN Whether to include the last N periods in the range or not.
	 *                         Ignored if period == range.
	 * 
	 * @ignore
	 */
	public static function getDateRangeForPeriod( $date, $period, $lastN = false )
	{
		if ($date === false)
		{
			return array(false, false);
		}
		
		// if the range is just a normal period (or the period is a range in which case lastN is ignored)
		if ($lastN === false
			|| $period == 'range')
		{
			if ($period == 'range')
			{
				$oPeriod = new Piwik_Period_Range('day', $date);
			}
			else
			{
				$oPeriod = Piwik_Period::factory($period, Piwik_Date::factory($date));
			}
			
			$startDate = $oPeriod->getDateStart();
			$endDate = $oPeriod->getDateEnd();
		}
		else // if the range includes the last N periods
		{
			list($date, $lastN) =
				Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution::getDateRangeAndLastN($period, $date, $lastN);
			list($startDate, $endDate) = explode(',', $date);
		
			$startDate = Piwik_Date::factory($startDate);
			$endDate = Piwik_Date::factory($endDate);
		}
		return array($startDate, $endDate);
	}
	
	/**
	 * Utility function, makes sure idSite string has only one site ID and throws if
	 * otherwise.
	 */
	private function checkSingleIdSite( $idSite, $extraMessage )
	{
		// can only add a note to one site
		if (!is_numeric($idSite))
		{
			throw new Exception("Invalid idSite: '$idSite'. $extraMessage");
		}
	}
	
	/**
	 * Utility function, makes sure date string is valid date, and throws if
	 * otherwise.
	 */
	private function checkDateIsValid( $date, $canBeNull = false )
	{
		if ($date === null
			&& $canBeNull)
		{
			return;
		}
		
		Piwik_Date::factory($date);
	}
}
